배열기반 다대다 관계에 중간테이블 도입 {mongodb}
문제정의
Prisma가 만들어준 M:N 관계 스키마를 사용하지 않고 Mongodb Compass에서 직접 Delete 작업을 수행하려니 고충이 너무 많이 든다. 그리고 Category SoftDelete를 수행하니 Academy.categoryIds 쪽에 아무런 일도 일어나지 않아 정합성이 깨져버린다. 이 문제를 어떻게 해결하면 좋을까?
ASIS
class AcademyCategoryRepository{
async deleteCategory(id: ObjectId) {
return await this.prisma.academyCategory.update({
where: { id },
data: {
deletedTime: new Date(),
},
});
}
}
model Academy {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
categoryIds String[] @db.ObjectId @default([])
categories AcademyCategory[] @relation(fields: [categoryIds], references: [id])
@@index([name], map: "name_1")
@@map("academymodels")
}
model AcademyCategory {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String @unique
description String?
createdTime DateTime @default(now()) @db.Date
updatedTime DateTime @updatedAt @db.Date
deletedTime DateTime? @db.Date
academyIds String[] @db.ObjectId
academies Academy[] @relation(fields: [academyIds], references: [id])
@@map("academycategorymodels")
}
AI Response: ChatGPT 4o
이 문제는 Prisma와 MongoDB의 M:N 관계 모델링 방식의 한계와 soft delete 방식이 맞물려 발생한 정합성 문제입니다. 구체적으로는 AcademyCategory
soft delete 시 해당 카테고리를 참조하는 Academy.categoryIds
에서의 정합성 유지를 Prisma가 자동으로 해주지 않기 때문에 발생합니다. 이를 해결하기 위한 두 가지 큰 방향이 있습니다:
✅ 권장 접근: 명시적 관계 갱신 처리
Soft delete를 수행할 때 직접 관련된 Academy
의 categoryIds
필드에서도 해당 Category.id
를 제거해야 정합성을 유지할 수 있습니다. 이를 위해 다음과 같은 명시적 처리 로직이 필요합니다.
🔧 개선된 Soft Delete 로직
class AcademyCategoryRepository {
async deleteCategory(id: ObjectId) {
// Step 1: Soft-delete the category
await this.prisma.academyCategory.update({
where: { id },
data: {
deletedTime: new Date(),
},
});
// Step 2: Remove category reference from all academies
await this.prisma.academy.updateMany({
where: {
categoryIds: {
has: id.toString(), // ObjectId는 string으로 저장됨
},
},
data: {
categoryIds: {
set: Prisma.sql`categoryIds.filter(cid => cid !== ${id.toString()})` as any, // or handle in app logic
},
},
});
}
}
하지만 Prisma에서는 MongoDB의 array 필드에서 특정 값을 삭제하는 native 지원이 부족하므로, updateMany
는 잘 안 먹힐 수 있습니다. 따라서 모든 academy를 조회 후 app 로직에서 배열 조작이 필요합니다:
🔁 안전한 Soft Delete with Manual Array Update
const academies = await this.prisma.academy.findMany({
where: {
categoryIds: {
has: id.toString(),
},
},
});
await Promise.all(
academies.map(academy =>
this.prisma.academy.update({
where: { id: academy.id },
data: {
categoryIds: academy.categoryIds.filter(
cid => cid !== id.toString(),
),
},
}),
),
);
🧼 차선책: Soft Delete 필터링 일관화 (정합성보단 UX 일관성)
AcademyCategory.deletedTime !== null
인 경우 모든 API 응답과 UI에서 제외시켜 정합성 보다는 UX 레벨 일관성을 지키는 방식입니다. 이 경우 Academy.categoryIds
에 존재하지만 실제 카테고리는 "삭제된 것처럼" 보이게 하므로, 데이터 정합성은 손해를 감수하는 대신 코드 간소화를 얻을 수 있습니다.
예시: Academy 조회시 join 필터링
const academy = await this.prisma.academy.findUnique({
where: { id },
include: {
categories: {
where: {
deletedTime: null,
},
},
},
});
단, 이 방식은 "카테고리가 삭제되었는데
Academy.categoryIds
에 여전히 존재하는 이상한 상태"를 DB 레벨에서 허용하기 때문에 추천하지 않습니다. 통합 정합성 유지가 중요하다면 위의 명시적 제거 로직을 사용하는 것이 좋습니다.
🔐 보너스 팁: M:N 모델링 개선 고려
현재 구조는 MongoDB에서는 추천되지 않는 방식입니다. MongoDB에서 Many-to-Many는 일반적으로 Join Table을 직접 설계하는 형태가 더 유지보수에 좋습니다.
예를 들어:
model AcademyCategoryAcademy {
id String @id @default(auto()) @map("_id") @db.ObjectId
academyId String @db.ObjectId
categoryId String @db.ObjectId
deletedTime DateTime? @db.Date
}
이런 중간 모델을 두면 정합성 유지 및 soft delete 시 유연성이 훨씬 높아집니다.
✨ 정리
방식 | 설명 | 장점 | 단점 |
---|---|---|---|
✅ 명시적 관계 해제 처리 | Soft delete 시 Academy의 categoryIds에서 제거 | 정합성 유지 | 로직 복잡 |
🧼 Soft delete만 필터링 | deletedTime 기반으로 UI/응답 필터 | 구현 간단 | 데이터 불일치 가능 |
🔁 M:N 중간 테이블 도입 | 중간 모델 생성 | 유연한 확장, 쿼리 명확 | 스키마 변경 필요 |
필요하다면 M:N 중간 테이블 방식으로 완전한 구조 개선까지 도와드릴게요.
혹시 지금 사용하는 구조에서 soft delete나 정합성 외에 고민되는 지점이 있나요?